<?php defined('SYSPATH') or die('No direct script access.');
/**
 * 会员认证 DB 处理
 *
 * @author  Yellow.Chow <aultoale@gmail.com>
 * @version $Id: db.php 1 2014-01-13 15:05:51Z zhouyl $
 * @license http://www.gnu.org/licenses/gpl-3.0.html    GPL 3
 */
class Auth_Db
{

    /**
     * 构造方法
     */
    public function __construct()
    {
        if (mt_rand(1, 30) === 1) // 清除过期数据
        {
            $this->delete_expired_tokens();
        }
    }

    /**
     * 获取会员信息的 SQL
     *
     * @return Database_Query_Builder_Select
     */
    public function member_sql()
    {
        return DB::select(
                'mem.member_id', 'mem.email', 'mem.nickname', 'mem.password', 'mem.reg_time', 'mem.reg_ip',
                'mem.last_time', 'mem.last_ip', 'mem.last_failed', 'mem.failed_nums', 'mem.status',
                'field.realname', 'field.mobile', 'field.region_id', 'field.address', 'field.avatar'
            )
            ->from(array('members', 'mem'))
            ->join(array('member_fields', 'field'), 'left')
            ->on('field.member_id', '=', 'mem.member_id')
            ->as_object(TRUE);
    }

    /**
     * 校验登录帐号及密码
     *
     * @param  string  $account   邮箱/昵称
     * @param  string  $password  登录密码(未加密)
     * @param  int     $lifetime  保存密码的周期
     * @return stdClass
     * @throws Auth_Exception
     */
    public function login($account, $password, $lifetime = FALSE)
    {
        // 转换为小写
        $account = strtolower(trim($account));

        // 检测登录帐号
        if (empty($account))
        {
            throw new Auth_Exception('邮箱或昵称不能为空');
        }

        // 检测密码
        if (empty($password))
        {
            throw new Auth_Exception('密码不能为空');
        }

        // 构造查询语句
        $sql = $this->member_sql();

        // 查询条件
        Valid::email($account) ?
            $sql->where('mem.email', '=', $account) :
            $sql->where('mem.nickname', '=', $account);

        // 取出会员信息
        $member = $sql->fetch_row();
        if ( ! is_object($member))
        {
            throw new Auth_Exception("输入的登录帐号不存在，请重新输入");
        }

        // 已锁定
        if ($member->status == Member::LOCKED)
        {
            throw new Auth_Exception('该帐号已经被锁定，请与管理员联系');
        }

        // 未激活
        if ($member->status == Member::NOTACT)
        {
            throw new Auth_Exception('您的帐号尚未激活，请检查您的邮箱完成激活操作。');
        }

        // 如果距离上次登录超过 5 分钟，则清零失败次数统计
        if ($member->last_failed + Auth::$failed_seconds < time())
        {
            $member->failed_nums = 0;
            $this->update(array('failed_nums' => 0), $member->member_id);
        }

        // 达到失败次数限制，且在时间范围内
        if ($member->failed_nums >= Auth::$failed_limit AND
           ($member->last_failed + Auth::$failed_seconds > time()))
        {
            $diff = $member->last_failed + Auth::$failed_seconds - time();
            throw new Auth_Exception('该帐号已锁定，请在 :seconds 分钟后重新登录',
                array(':seconds' => ceil(fmod($diff / 60, 60))));
        }

        // 校验密码
        if (Password::match($member->password, $password))
        {
            $data = array(
                'failed_nums' => 0,
                'last_ip'     => Request::$client_ip,
                'last_time'   => time(),
            );

            foreach ($data as $key => $value)
            {
                $member->$key = $value;
            }

            // 保存登录状态
            if ($lifetime)
            {
                $this->remember($member->member_id, ($lifetime === TRUE) ? 1209600 : $lifetime);
            }
            else
            {
                Cookie::delete(Auth::$cookie_token);
            }

            // 重置 Cookie 中的失败次数
            $this->failures(0);

            // 更新会员数据
            $this->update($data, $member->member_id);

            // 更新登录统计
            $this->update_logins($member->member_id);
        }
        else // 密码错误
        {
            // 在 Cookie 中记录失败次数
            $this->failures(1);

            $data = array(
                'failed_nums' => $member->failed_nums + 1,
                'last_failed' => time(),
            );

            // 更新会员数据
            $this->update($data, $member->member_id);

            if (Auth::$failed_seconds) // 时间限制大于 0
            {
                if ($data['failed_nums'] >= Auth::$failed_limit) // 达到次数限制
                {
                    $diff = $data['last_failed'] + Auth::$failed_seconds - time();
                    throw new Auth_Exception('该帐号已锁定，请在 :seconds 分钟后重新登录',
                        array(':seconds' => ceil(fmod($diff / 60, 60))));
                }

                $limit = Auth::$failed_limit - $data['failed_nums'];
                throw new Auth_Exception('登录帐号和密码不匹配，您还可尝试 :limit 次',
                    array(':limit' => $limit > 0 ? $limit : 0));
            }
            else
            {
                throw new Auth_Exception('您输入的登录帐号和密码不匹配，请重新输入');
            }
        }

        return $member;
    }

    /**
     * 返回登录验证失败次数
     *
     * @param  int  $nums  设置为小于 1 时，重置失败次数
     * @return int
     */
    public function failures($nums = NULL)
    {
        if ($nums === NULL)
        {
            return Cookie::get('login_failures', 0);
        }

        if ($nums < 1)
        {
            return Cookie::delete('login_failures');
        }

        return Cookie::set('login_failures', Cookie::get('login_failures', 0) + $nums, 3600);
    }

    /**
     * 会员自动登录
     *
     * @param  string  $token  加密标识
     * @return stdClass
     */
    public function auto_login($token)
    {
        if (preg_match('/^[a-f0-9]{32}$/', $token))
        {
            $token = DB::select()
                ->from('member_tokens')
                ->where('token', '=', $token)
                ->as_object(TRUE)
                ->fetch_row();
            if (is_object($token) AND $token->expires > time() AND
               ($token->user_agent === sha1(Request::$user_agent)))
            {
                // 取出会员信息
                $member = $this->member_sql()
                    ->where('mem.member_id', '=', $token->member_id)
                    ->fetch_row();
                if (is_object($member) AND $member->status != Member::LOCKED) // 未锁定
                {
                    $this->remember($token->member_id, $token->expires - time());

                    return $member;
                }
            }
        }

        return FALSE;
    }

    /**
     * 保存会员登录记录
     *
     * @param  int  $member_id
     * @param  int  $lifetime   保存密码的周期，默认为2周，0表示删除已有标识
     */
    public function remember($member_id, $lifetime = 1209600)
    {
        // 删除已有标识
        $this->delete_token($member_id);

        if ($lifetime <= 0) return ;

        // 写入数据库
        $data = array(
            'member_id'  => $member_id,
            'user_agent' => sha1(Request::$user_agent),
            'token'      => md5(time().uniqid().$member_id),
            'created'    => time(),
            'expires'    => time() + $lifetime,
        );
        DB::insert('member_tokens', $data)->execute();

        // 设置 cookie 数据
        Cookie::set(Auth::$cookie_token, $data['token'], $lifetime);
    }

    /**
     * 删除指定会员的登录标识
     *
     * @param  int  $member_id
     */
    public function delete_token($member_id)
    {
        DB::delete('member_tokens')
            ->where('member_id', '=', $member_id)
            ->execute();

        // 删除 cookie
        Cookie::delete(Auth::$cookie_token);
    }

    /**
     * 删除所有的过期登录标识
     */
    public function delete_expired_tokens()
    {
        DB::delete('member_tokens')
            ->where('expires', '<', time())
            ->execute();
    }

    /**
     * 更新会员信息
     *
     * @param  array  $set
     * @param  int    $member_id
     */
    public function update(array $data, $member_id)
    {
        if ($member_id <= 0) return ;

        $cols = array('last_ip', 'last_time', 'last_failed', 'failed_nums');

        $set = array();
        foreach ($data as $key => $value)
        {
            if ( ! in_array($key, $cols))
            {
                continue;
            }
            $set[$key] = $value;
        }

        // 更新会员数据
        DB::update('members', $set)
            ->where('member_id', '=', $member_id)
            ->execute();
    }

    /**
     * 更新登录统计
     *
     * @param  int  $member_id
     */
    public function update_logins($member_id)
    {
        if ( ! $member_id) return ;

        $data = array(
            'member_id'  => $member_id,
            'login_time' => time(),
            'ip'         => Request::$client_ip,
        );

        DB::insert('member_logins', $data)->execute();
    }

}